package com.g0dkar.leet.util.file;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessControlException;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.g0dkar.leet.log.Log;
import com.g0dkar.leet.util.common.StringUtils;

/**
 * This class contains many useful methods to ease the job of working with files. Especially text files.
 * 
 * @author g0dkar
 * 
 */
public final class FileUtils {
	private static Map<String, FileEntry> FILE_ENTRIES = null;
	
	public static final String APPLICATION_DIRECTORY;
	
	static {
		APPLICATION_DIRECTORY = System.getProperty("user.dir");
	}
	
	private FileUtils() throws InstantiationException {
		throw new InstantiationException("You should not be seeing this. Don't try to instantiate this, please.");
	}
	
	private static Enumeration<URL> EMPTY_ENUMERATION = new Enumeration<URL>() {
		public boolean hasMoreElements() {
			return false;
		}
		
		public URL nextElement() {
			return null;
		}
	};
	
	/**
	 * Retorna objeto {@link File} correspondente ao nome de arquivo especificado. Primeiro este método tenta encontrar
	 * o recurso através do S.O. hospedeiro. Se o recurso não existir, tenta-se procurá-lo nos recursos da JVM. Se ainda
	 * assim não for encontrado, é retornado <code>null</code>.
	 * 
	 * <em>Este método é compatível com a Google App Engine.</em>
	 * 
	 * @param fileName
	 *            Nome completo do recurso
	 * @return {@link File} do recurso ou <code>null</code> caso ele não exista, ou aconteça uma
	 *         {@link URISyntaxException} ao tentar converter uma URL em URI.
	 * 
	 * @see File#File(String)
	 * @see ClassLoader#getSystemResource(String)
	 * @see URL#toURI()
	 */
	public static File getFile(String fileName) {
		File file = new File(fileName);
		
		try {
			if (file.exists()) {
				return file;
			}
		} catch (AccessControlException e) {
			// TODO log file permission denied
		}
		
		try {
			URL resource = getResource(fileName);
			if (resource != null) {
				return new File(resource.toURI());
			}
		} catch (URISyntaxException e) {
			// TODO Nothing?
		} catch (NullPointerException e) {
			return null;
		}
		
		return null;
	}
	
	/**
	 * Searches given resource name through current {@link ClassLoader}.
	 * 
	 * @param name
	 *            - {@link URI} of the resource
	 * @return {@link URL} of the found resource, or null if the resource can't be found
	 */
	public static URL getResource(String name) {
		return Thread.currentThread().getContextClassLoader().getResource(name);
	}
	
	/**
	 * Searches given resource name through {@link ClassLoader}.
	 * 
	 * @param name
	 *            - {@link URI} of the resource
	 * @return {@link InputStream} reading from resource, or null if the resource can't be found
	 */
	public static InputStream getResourceAsStream(String name) {
		return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
	}
	
	public static boolean exists(String file) {
		return getResource(file) != null;
	}
	
	/**
	 * Returns the file's content in a {@link StringBuilder}.
	 * 
	 * @param fileName
	 *            The file which'll be read
	 * @return The file's content within a {@link StringBuilder}, or <code>null</code> if it do not exist or cannot be
	 *         read.
	 * @throws IOException
	 *             The file could not be read for some reason.
	 */
	public static StringBuilder getText(String fileName) {
		InputStream inputStream = getResourceAsStream(fileName);
		StringBuilder str = null;
		
		if (inputStream != null) {
			try {
				str = new StringBuilder();
				String line;
				BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
				
				while ((line = br.readLine()) != null) {
					str.append(line);
					str.append(StringUtils.LINE_SEPARATOR);
				}
				
				// Removes the extra LINE_SEPARATOR
				str.deleteCharAt(str.length() - 1);
				
				// And close the stream
				br.close();
			} catch (IOException e) {
				// TODO Should we "throw new JHeatRuntimeException(e);"?
				return null;
			}
		}
		
		return str;
	}
	
	/**
	 * Writes given str to the file named with given filename.
	 * 
	 * @param str
	 *            - contents to write
	 * @param filename
	 *            - name of target file
	 */
	public static void save(String str, String filename) {
		/*
		 * This Method is NOT supported by the GAE
		 * 
		 * File file = new File(filename); file.getParentFile().mkdirs(); FileWriter writer = null;
		 * 
		 * try { writer = new FileWriter(file); // Jackson FIXME: O arquivo está sendo gravado com a codificação do //
		 * SO e não da String str. writer.append(str); } catch (IOException e) { throw new JHeatRuntimeException(e); }
		 * finally { try { writer.close(); } catch (IOException e) { JHeatLogger.error("Error closing file: " +
		 * filename); } }
		 */
	}
	
	/**
	 * Helper method for <code>touch(new File(file))</code>.
	 * 
	 * @param file
	 *            The file path
	 * @return <code>true</code> if the file could be touched, false otherwise.
	 * @see #touch(File)
	 */
	public static boolean touch(String file) {
		return touch(new File(file));
	}
	
	/**
	 * Tries to <em>touch</em> a file. Simply checks if it exists and if not, creates the file on the file system.
	 * 
	 * This code tries to do the same as the <code>touch</code> UNIX command.
	 * 
	 * For more information, read <a href="http://linux.about.com/od/commands/l/blcmdl1_touch.htm"
	 * title="touch Linux command on About.com">this article</a>.
	 * 
	 * @param file
	 *            The file descriptor
	 * @return <code>true</code> if the file could be touched, false otherwise.
	 * @see File#createNewFile()
	 */
	public static boolean touch(File file) {
		try {
			if (file != null) {
				File parent = file.getParentFile();
				
				if (parent != null) {
					if (parent.exists() || parent.mkdirs()) {
						return file.createNewFile();
					}
				}
				else {
					return file.createNewFile();
				}
			}
			
			return false;
		} catch (IOException e) {
			return false;
		}
	}
	
	/**
	 * Lists all resources inside pkgName, searching in all jars within the classpath.
	 * 
	 * @param pkgName
	 *            - the name of the base package, <b>required</b>
	 */
	public static List<String> listResources(String pkgName) {
		LinkedList<String> list = new LinkedList<String>();
		pkgName = pkgName.replace(".", "/");
		Enumeration<URL> resources = FileUtils.getResources(pkgName);
		while (resources.hasMoreElements()) {
			URL url = resources.nextElement();
			String path = url.getPath().replace("%20", " ");
			try {
				List<String> subResources;
				if (path.contains(".jar!")) {
					path = extractFilePath(path);
					subResources = listJarResources(new JarFile(path), pkgName);
				}
				else {
					subResources = listDirResources(path, pkgName);
				}
				list.addAll(subResources);
			} catch (IOException e) {
				Log.error("Resources not read from: {} ~ {}", path, e.toString());
			}
		}
		return list;
	}
	
	/**
	 * Converts a resource file URL to a file path.<br>
	 * Ex. from "file:/var/lib/test.jar!org/test/properties" to "/var/lib/test.jar" from
	 * "file:/D:/workspace/lib/test.jar!org/test/properties" to "D:\workspace\lib\test.jar"
	 * 
	 * @param jarFile
	 */
	static String extractFilePath(String jarFile) {
		String fileName = jarFile.split("!", 2)[0].split(":", 2)[1];
		return nativePath(fileName);
	}
	
	private static String nativePath(String path) {
		if (File.separatorChar == '/') {
			return path;
		}
		else {
			return path.substring(1).replace('/', '\\');
		}
	}
	
	private static String resourcePath(String path) {
		return path.replace('\\', '/');
	}
	
	/**
	 * @param name
	 * @return All resources with given name in a {@link Enumeration}, or an empty {@link Enumeration} if nothing is
	 *         found.
	 */
	static Enumeration<URL> getResources(String name) {
		try {
			return Thread.currentThread().getContextClassLoader().getResources(name);
		} catch (IOException e1) {
			return EMPTY_ENUMERATION;
		}
	}
	
	/**
	 * @param jar
	 * @param prefix
	 * @return a {@link List} of jar entries names, which then can be used with {@link #getResource(String)}
	 */
	static List<String> listJarResources(JarFile jar, String prefix) {
		Enumeration<JarEntry> entries = jar.entries();
		LinkedList<String> list = new LinkedList<String>();
		while (entries.hasMoreElements()) {
			JarEntry entry = entries.nextElement();
			String name = entry.getName();
			if (StringUtils.isEmpty(prefix) || name.startsWith(prefix) && !name.endsWith("/")) {
				list.add(name);
			}
		}
		return list;
	}
	
	/**
	 * @param path
	 * @param pkgName
	 * @return A {@link List} of all resources names in given path, which then can be used with
	 *         {@link #getResource(String)}.<br>
	 *         All file paths are converted to resource paths, stripping the root directory's base path from the file
	 *         paths.
	 */
	static List<String> listDirResources(String path, String pkgName) {
		List<String> paths = new LinkedList<String>();
		File file = new File(path);
		String rootPath = path.substring(0, path.lastIndexOf(pkgName));
		rootPath = nativePath(rootPath);
		addDirResources(file, rootPath, paths);
		return paths;
	}
	
	/**
	 * If the file is a directory, adds all files names inside to the list paths, otherwise add the name of the file.<br>
	 * All file paths are converted to resource paths, stripping the root directory's base path from the file paths.
	 * 
	 * @param file
	 * @param rootPath
	 * @param paths
	 */
	static void addDirResources(File file, String rootPath, List<String> paths) {
		if (file.isDirectory()) {
			for (File child : file.listFiles()) {
				addDirResources(child, rootPath, paths);
			}
		}
		else {
			String absPath = file.getPath();
			String relPath = absPath.substring(rootPath.length());
			paths.add(resourcePath(relPath));
		}
	}
	
	public static class FileEntry {
		public final File file;
		public final JarEntry jarEntry;
		
		private FileEntry(File entry) {
			file = entry;
			jarEntry = null;
		}
		
		private FileEntry(JarEntry entry) {
			file = null;
			jarEntry = entry;
		}
		
		public boolean isFile() {
			return file != null;
		}
		
		public boolean isJarFile() {
			return jarEntry != null;
		}
	}
}
